Skip to content

Conversation

@stevespringett
Copy link
Member

No description provided.

Signed-off-by: Steve Springett <[email protected]>
@stevespringett stevespringett added this to the 2.0 milestone Jun 15, 2025
@stevespringett stevespringett self-assigned this Jun 15, 2025
@stevespringett stevespringett added the CDX 2.0 related to release v2.0 label Jun 15, 2025
@stevespringett stevespringett linked an issue Jun 15, 2025 that may be closed by this pull request
@jkowalleck jkowalleck changed the title CycloneDX v2.0 Specification [WIP] CycloneDX v2.0 Specification Jun 16, 2025
const absoluteRootPath = path.resolve(rootSchemaPath);

// Verify paths exist
await fs.access(absoluteModelsDir);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

To fix the problem, we should validate that the directory supplied by the user (modelsDirectory) stays within an allowed root folder, or at least is not outside the current working directory if no stricter root is configured. The best approach for this CLI tool is to resolve the modelsDirectory input relative to the current working directory, normalize it with fs.realpathSync() or fs.promises.realpath, and then check that the resulting path is a subdirectory of a chosen safe root (for this CLI, the process's current working directory is reasonable). If the path escapes this root, the program should error out before continuing. The changes needed are:

  • In the bundleSchemas function, replace the logic on lines 143-149 to:
    • Normalize and resolve the modelsDirectory path, eliminating symbolic links.
    • Check that the resolved directory is contained within the current working directory (or other configurable root, if needed).
    • If not, throw an error or exit with a clear message.
  • Add any imports necessary for this logic (none beyond those already present).
  • These changes are all local to the code block shown—in tools/src/main/js/bundle-schemas.js.
Suggested changeset 1
tools/src/main/js/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundle-schemas.js b/tools/src/main/js/bundle-schemas.js
--- a/tools/src/main/js/bundle-schemas.js
+++ b/tools/src/main/js/bundle-schemas.js
@@ -140,7 +140,12 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
+        // Normalize and check modelsDirectory to avoid path traversal
+        const cwd = process.cwd();
+        const absoluteModelsDir = await fs.realpath(path.resolve(modelsDirectory));
+        if (!absoluteModelsDir.startsWith(cwd)) {
+            throw new Error(`Models directory must be within the current working directory: ${cwd}`);
+        }
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
         // Verify paths exist
EOF
@@ -140,7 +140,12 @@

async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
// Normalize and check modelsDirectory to avoid path traversal
const cwd = process.cwd();
const absoluteModelsDir = await fs.realpath(path.resolve(modelsDirectory));
if (!absoluteModelsDir.startsWith(cwd)) {
throw new Error(`Models directory must be within the current working directory: ${cwd}`);
}
const absoluteRootPath = path.resolve(rootSchemaPath);

// Verify paths exist
Copilot is powered by AI and may make mistakes. Always verify output.

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

To fix this issue, we need to ensure that user-supplied paths (modelsDirectory and rootSchemaPath) used by the tool are confined within an expected directory tree. Since the intended use is to process files in a specific models directory, it is safest to verify that the supplied rootSchemaPath resolves to a file inside modelsDirectory, and that both paths are normalized and checked for containment.

The best way to do this is:

  • Normalize both paths using path.resolve and fs.realpathSync (to resolve symlinks).
  • After normalization, check that absoluteRootPath starts with absoluteModelsDir (possibly with path separator handling).
  • If the check fails, report an error to the user and halt execution, thus preventing access to unexpected resources.

Only make changes within the code region shown in tools/src/main/js/bundle-schemas.js, adding any required imports (e.g., using fs for realpathSync). Use synchronous calls for containment check before async access, as this avoids a TOCTOU race and keeps logic simple.


Suggested changeset 1
tools/src/main/js/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundle-schemas.js b/tools/src/main/js/bundle-schemas.js
--- a/tools/src/main/js/bundle-schemas.js
+++ b/tools/src/main/js/bundle-schemas.js
@@ -2,6 +2,7 @@
 
 const fs = require('fs').promises;
 const path = require('path');
+const fsSync = require('fs');
 
 function isObject(value) {
     return typeof value === 'object' && value !== null;
@@ -143,16 +144,32 @@
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
-        // Verify paths exist
-        await fs.access(absoluteModelsDir);
-        await fs.access(absoluteRootPath);
+        // Canonicalize with realpathSync to resolve symlinks (synchronously to avoid TOCTOU)
+        let realModelsDir, realRootPath;
+        try {
+            realModelsDir = fsSync.realpathSync(absoluteModelsDir);
+            realRootPath = fsSync.realpathSync(absoluteRootPath);
+        } catch (err) {
+            throw new Error('Could not resolve one of the provided paths: ' + err.message);
+        }
 
-        const rootSchemaFilename = path.basename(absoluteRootPath);
-        const rootSchemaDir = path.dirname(absoluteRootPath);
+        // Ensure root schema file is inside the models directory
+        // Add path.sep to avoid partial prefix matches
+        const modelsRootPrefix = realModelsDir.endsWith(path.sep) ? realModelsDir : realModelsDir + path.sep;
+        if (!realRootPath.startsWith(modelsRootPrefix)) {
+            throw new Error(`The root schema path must be inside the models directory. Given: ${realRootPath} not under ${realModelsDir}`);
+        }
 
-        console.log(`Models directory: ${absoluteModelsDir}`);
-        console.log(`Root schema: ${absoluteRootPath}`);
+        // Verify paths exist (async access, after check)
+        await fs.access(realModelsDir);
+        await fs.access(realRootPath);
 
+        const rootSchemaFilename = path.basename(realRootPath);
+        const rootSchemaDir = path.dirname(realRootPath);
+
+        console.log(`Models directory: ${realModelsDir}`);
+        console.log(`Root schema: ${realRootPath}`);
+
         // Generate output filenames
         const baseFilename = rootSchemaFilename.replace('.schema.json', '');
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
EOF
@@ -2,6 +2,7 @@

const fs = require('fs').promises;
const path = require('path');
const fsSync = require('fs');

function isObject(value) {
return typeof value === 'object' && value !== null;
@@ -143,16 +144,32 @@
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
// Canonicalize with realpathSync to resolve symlinks (synchronously to avoid TOCTOU)
let realModelsDir, realRootPath;
try {
realModelsDir = fsSync.realpathSync(absoluteModelsDir);
realRootPath = fsSync.realpathSync(absoluteRootPath);
} catch (err) {
throw new Error('Could not resolve one of the provided paths: ' + err.message);
}

const rootSchemaFilename = path.basename(absoluteRootPath);
const rootSchemaDir = path.dirname(absoluteRootPath);
// Ensure root schema file is inside the models directory
// Add path.sep to avoid partial prefix matches
const modelsRootPrefix = realModelsDir.endsWith(path.sep) ? realModelsDir : realModelsDir + path.sep;
if (!realRootPath.startsWith(modelsRootPrefix)) {
throw new Error(`The root schema path must be inside the models directory. Given: ${realRootPath} not under ${realModelsDir}`);
}

console.log(`Models directory: ${absoluteModelsDir}`);
console.log(`Root schema: ${absoluteRootPath}`);
// Verify paths exist (async access, after check)
await fs.access(realModelsDir);
await fs.access(realRootPath);

const rootSchemaFilename = path.basename(realRootPath);
const rootSchemaDir = path.dirname(realRootPath);

console.log(`Models directory: ${realModelsDir}`);
console.log(`Root schema: ${realRootPath}`);

// Generate output filenames
const baseFilename = rootSchemaFilename.replace('.schema.json', '');
const bundledFilename = `${baseFilename}-bundled.schema.json`;
Copilot is powered by AI and may make mistakes. Always verify output.
console.log(`Output: ${outputPath}\n`);

// Read all schema files in the models directory
const files = await fs.readdir(absoluteModelsDir);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

The best fix is to restrict the modelsDirectory parameter to a safe root directory. After resolving the provided path, check if the normalized, resolved path is contained within an intended root boundary (e.g., the current working directory or a specific, hardcoded directory). If the resolved path is outside this root, terminate the process with an error message. This can be achieved by:

  1. Defining a root directory (for example, process.cwd() or an explicit path).
  2. Using path.resolve() to normalize both the root directory and user-provided directory.
  3. Comparing them: ensuring that the user-specified directory begins with the normalized root directory path.
  4. Failing the operation if the check does not pass.

These changes are to be made in bundleSchemas, immediately after calculating the resolved path of modelsDirectory (line 143).

This fix requires no new methods but does require one additional code block for the path check.


Suggested changeset 1
tools/src/main/js/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundle-schemas.js b/tools/src/main/js/bundle-schemas.js
--- a/tools/src/main/js/bundle-schemas.js
+++ b/tools/src/main/js/bundle-schemas.js
@@ -141,6 +141,10 @@
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
         const absoluteModelsDir = path.resolve(modelsDirectory);
+        const safeRootDir = path.resolve(process.cwd()); // restrict to working directory
+        if (!absoluteModelsDir.startsWith(safeRootDir + path.sep)) {
+            throw new Error(`Unsafe models directory: ${modelsDirectory}\nMust be within ${safeRootDir}`);
+        }
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
         // Verify paths exist
EOF
@@ -141,6 +141,10 @@
async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const safeRootDir = path.resolve(process.cwd()); // restrict to working directory
if (!absoluteModelsDir.startsWith(safeRootDir + path.sep)) {
throw new Error(`Unsafe models directory: ${modelsDirectory}\nMust be within ${safeRootDir}`);
}
const absoluteRootPath = path.resolve(rootSchemaPath);

// Verify paths exist
Copilot is powered by AI and may make mistakes. Always verify output.
const schemaPath = path.join(absoluteModelsDir, file);
console.log(` Reading ${file}...`);

const content = await fs.readFile(schemaPath, 'utf8');

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

// Read the root schema
console.log(`\nReading root schema...`);
const rootContent = await fs.readFile(absoluteRootPath, 'utf8');

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

To fix the problem, we need to ensure that any file path provided by the user is validated or sanitized before it is used in file operations. Specifically, we should ensure that rootSchemaPath and any other user-supplied file paths are contained within a designated root folder (for example, the models directory or the current working directory). This is best accomplished by fully resolving the user input to an absolute path, then ensuring that it resides under a "safe" directory.

The fix will:

  • Normalize user-supplied paths using path.resolve.
  • Use fs.realpath or fs.realpathSync to account for symlinks.
  • Check that the absolute path starts with the safe root directory (e.g., the resolved models directory or the current working directory).
  • Refuse to operate (error and exit) if the path is outside the allowable directory.

Changes will be made to tools/src/main/js/bundle-schemas.js, specifically in the CLI entry point and the beginning of the bundleSchemas function, to validate modelsDirectory and rootSchemaPath before attempting any file operations.

Additionally, since the code uses fs.promises, we'll use fs.realpath (async) for canonicalization.


Suggested changeset 1
tools/src/main/js/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundle-schemas.js b/tools/src/main/js/bundle-schemas.js
--- a/tools/src/main/js/bundle-schemas.js
+++ b/tools/src/main/js/bundle-schemas.js
@@ -140,8 +140,21 @@
 
 async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
     try {
-        const absoluteModelsDir = path.resolve(modelsDirectory);
-        const absoluteRootPath = path.resolve(rootSchemaPath);
+        // Canonicalize the directories
+        const absoluteModelsDir = await fs.realpath(path.resolve(modelsDirectory));
+        const absoluteRootPathUnverified = path.resolve(rootSchemaPath);
+        // Canonicalize (resolve symlinks)
+        let absoluteRootPath;
+        try {
+            absoluteRootPath = await fs.realpath(absoluteRootPathUnverified);
+        } catch (e) {
+            throw new Error(`Root schema file does not exist: ${absoluteRootPathUnverified}`);
+        }
+        // Security: ensure root schema path is within modelsDirectory
+        // Or, less restrictively, within the process cwd; here we choose modelsDirectory
+        if (!absoluteRootPath.startsWith(absoluteModelsDir + path.sep)) {
+            throw new Error(`Root schema path (${absoluteRootPath}) is outside of models directory (${absoluteModelsDir})`);
+        }
 
         // Verify paths exist
         await fs.access(absoluteModelsDir);
EOF
@@ -140,8 +140,21 @@

async function bundleSchemas(modelsDirectory, rootSchemaPath, options = {}) {
try {
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);
// Canonicalize the directories
const absoluteModelsDir = await fs.realpath(path.resolve(modelsDirectory));
const absoluteRootPathUnverified = path.resolve(rootSchemaPath);
// Canonicalize (resolve symlinks)
let absoluteRootPath;
try {
absoluteRootPath = await fs.realpath(absoluteRootPathUnverified);
} catch (e) {
throw new Error(`Root schema file does not exist: ${absoluteRootPathUnverified}`);
}
// Security: ensure root schema path is within modelsDirectory
// Or, less restrictively, within the process cwd; here we choose modelsDirectory
if (!absoluteRootPath.startsWith(absoluteModelsDir + path.sep)) {
throw new Error(`Root schema path (${absoluteRootPath}) is outside of models directory (${absoluteModelsDir})`);
}

// Verify paths exist
await fs.access(absoluteModelsDir);
Copilot is powered by AI and may make mistakes. Always verify output.
// Write bundled (pretty) version
console.log('\nWriting bundled schema...');
await fs.writeFile(bundledPath, JSON.stringify(finalSchema, null, 2));
const bundledStats = await fs.stat(bundledPath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

To fix this problem, user-supplied paths (namely, modelsDirectory and rootSchemaPath) must be validated to ensure that all constructed output files are written only inside a trusted root directory. The best way, as recommended, is to first resolve all user-supplied paths and then check that the resulting output paths (bundledPath, minifiedPath) reside inside the trusted models directory (or a stricter, predetermined directory). Specifically:

  • Calculate the absolute paths for both input directory and schema file.
  • When constructing the output path for the bundled and minified schemas, resolve them as absolute paths.
  • Before writing, verify that the paths of the files to be written start with the absolute trusted directory path. Abort with an error if any path escapes the safe area.
  • Add this validation before file writes in the method.

This fix applies only to the bundleSchemas function in tools/src/main/js/bundle-schemas.js. Changes required:

  1. Implement a function isSubPath(parent, child) to check if child is within parent.
  2. Before any file writes (specifically fs.writeFile(bundledPath, ...) and fs.writeFile(minifiedPath, ...)), check that bundledPath and minifiedPath are subpaths of absoluteModelsDir.
  3. Abort with a clear error if the check fails.

Suggested changeset 1
tools/src/main/js/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundle-schemas.js b/tools/src/main/js/bundle-schemas.js
--- a/tools/src/main/js/bundle-schemas.js
+++ b/tools/src/main/js/bundle-schemas.js
@@ -143,6 +143,13 @@
         const absoluteModelsDir = path.resolve(modelsDirectory);
         const absoluteRootPath = path.resolve(rootSchemaPath);
 
+        // Helper to check that child is within parent directory
+        function isSubPath(parent, child) {
+            const parentPath = path.resolve(parent) + path.sep;
+            const childPath = path.resolve(child);
+            return childPath.startsWith(parentPath);
+        }
+
         // Verify paths exist
         await fs.access(absoluteModelsDir);
         await fs.access(absoluteRootPath);
@@ -161,6 +168,14 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        // Validate output paths are within models directory
+        if (!isSubPath(absoluteModelsDir, bundledPath)) {
+            throw new Error(`Refusing to write bundled schema outside models directory: ${bundledPath}`);
+        }
+        if (!isSubPath(absoluteModelsDir, minifiedPath)) {
+            throw new Error(`Refusing to write minified schema outside models directory: ${minifiedPath}`);
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
@@ -288,6 +303,10 @@
         // Write bundled (pretty) version
         console.log('\nWriting bundled schema...');
         const prettyJson = JSON.stringify(finalSchema, null, 2);
+        // Double-check path before write
+        if (!isSubPath(absoluteModelsDir, bundledPath)) {
+            throw new Error(`Refusing to write bundled schema outside models directory: ${bundledPath}`);
+        }
         await fs.writeFile(bundledPath, prettyJson);
         const bundledStats = await fs.stat(bundledPath);
         const bundledSizeKB = (bundledStats.size / 1024).toFixed(2);
@@ -302,6 +321,10 @@
         const lineCount = minifiedJson.split('\n').length;
         console.log(`  Minified JSON is on ${lineCount} line(s)`);
 
+        // Double-check path before write
+        if (!isSubPath(absoluteModelsDir, minifiedPath)) {
+            throw new Error(`Refusing to write minified schema outside models directory: ${minifiedPath}`);
+        }
         await fs.writeFile(minifiedPath, minifiedJson);
         const minifiedStats = await fs.stat(minifiedPath);
         const minifiedSizeKB = (minifiedStats.size / 1024).toFixed(2);
EOF
@@ -143,6 +143,13 @@
const absoluteModelsDir = path.resolve(modelsDirectory);
const absoluteRootPath = path.resolve(rootSchemaPath);

// Helper to check that child is within parent directory
function isSubPath(parent, child) {
const parentPath = path.resolve(parent) + path.sep;
const childPath = path.resolve(child);
return childPath.startsWith(parentPath);
}

// Verify paths exist
await fs.access(absoluteModelsDir);
await fs.access(absoluteRootPath);
@@ -161,6 +168,14 @@
const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);

// Validate output paths are within models directory
if (!isSubPath(absoluteModelsDir, bundledPath)) {
throw new Error(`Refusing to write bundled schema outside models directory: ${bundledPath}`);
}
if (!isSubPath(absoluteModelsDir, minifiedPath)) {
throw new Error(`Refusing to write minified schema outside models directory: ${minifiedPath}`);
}

console.log(`Output (bundled): ${bundledPath}`);
console.log(`Output (minified): ${minifiedPath}\n`);

@@ -288,6 +303,10 @@
// Write bundled (pretty) version
console.log('\nWriting bundled schema...');
const prettyJson = JSON.stringify(finalSchema, null, 2);
// Double-check path before write
if (!isSubPath(absoluteModelsDir, bundledPath)) {
throw new Error(`Refusing to write bundled schema outside models directory: ${bundledPath}`);
}
await fs.writeFile(bundledPath, prettyJson);
const bundledStats = await fs.stat(bundledPath);
const bundledSizeKB = (bundledStats.size / 1024).toFixed(2);
@@ -302,6 +321,10 @@
const lineCount = minifiedJson.split('\n').length;
console.log(` Minified JSON is on ${lineCount} line(s)`);

// Double-check path before write
if (!isSubPath(absoluteModelsDir, minifiedPath)) {
throw new Error(`Refusing to write minified schema outside models directory: ${minifiedPath}`);
}
await fs.writeFile(minifiedPath, minifiedJson);
const minifiedStats = await fs.stat(minifiedPath);
const minifiedSizeKB = (minifiedStats.size / 1024).toFixed(2);
Copilot is powered by AI and may make mistakes. Always verify output.
// Write minified version
console.log('Writing minified schema...');
await fs.writeFile(minifiedPath, JSON.stringify(finalSchema));
const minifiedStats = await fs.stat(minifiedPath);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

The best and least intrusive fix is to ensure that any user-supplied path (modelsDirectory and/or rootSchemaPath) is validated before use, ensuring that the output files will only be created inside an intended safe directory (such as the models directory or another configured base root). After resolving the input paths to absolute paths, check that the output files (bundledPath, minifiedPath) are actually beneath the intended directory. If not, abort with a suitable error. This can be implemented by using path.resolve and then verifying that the output path starts with the intended root directory path. This fix should be applied in the bundleSchemas function, right after output paths are built but before files are written.

You need to:

  • After constructing bundledPath and minifiedPath, check that these are inside the expected output directory (likely absoluteModelsDir or, if required, a subdirectory of it).
  • If not, throw an error or exit.
  • Optionally add a helper function for containment check.
  • Import any necessary packages (though the standard library suffices here).

The edit is fully local to the snippet shown, within the definition of bundleSchemas.


Suggested changeset 1
tools/src/main/js/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundle-schemas.js b/tools/src/main/js/bundle-schemas.js
--- a/tools/src/main/js/bundle-schemas.js
+++ b/tools/src/main/js/bundle-schemas.js
@@ -161,6 +161,15 @@
         const bundledPath = path.join(rootSchemaDir, bundledFilename);
         const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
 
+        // Ensure output files are within the models directory (prevent path traversal)
+        function isSubPath(parent, child) {
+            const rel = path.relative(parent, child);
+            return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
+        }
+        if (!isSubPath(absoluteModelsDir, bundledPath) || !isSubPath(absoluteModelsDir, minifiedPath)) {
+            throw new Error('Output path is outside the models directory. Refusing to write output files.');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
 
EOF
@@ -161,6 +161,15 @@
const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);

// Ensure output files are within the models directory (prevent path traversal)
function isSubPath(parent, child) {
const rel = path.relative(parent, child);
return !!rel && !rel.startsWith('..') && !path.isAbsolute(rel);
}
if (!isSubPath(absoluteModelsDir, bundledPath) || !isSubPath(absoluteModelsDir, minifiedPath)) {
throw new Error('Output path is outside the models directory. Refusing to write output files.');
}

console.log(`Output (bundled): ${bundledPath}`);
console.log(`Output (minified): ${minifiedPath}\n`);

Copilot is powered by AI and may make mistakes. Always verify output.
// Write bundled (pretty) version
console.log('\nWriting bundled schema...');
const prettyJson = JSON.stringify(finalSchema, null, 2);
await fs.writeFile(bundledPath, prettyJson);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.
const lineCount = minifiedJson.split('\n').length;
console.log(` Minified JSON is on ${lineCount} line(s)`);

await fs.writeFile(minifiedPath, minifiedJson);

Check failure

Code scanning / CodeQL

Uncontrolled data used in path expression High

This path depends on a
user-provided value
.

Copilot Autofix

AI 1 day ago

To fix the problem, we must ensure that any user-supplied path does not "escape" a trusted base directory. This can be done by choosing a safe root directory (recommended to be the parent of the root schema), then verifying that all generated output paths (bundledPath, minifiedPath) are strictly located within this directory. To do this, after normalizing both the computed output path and the chosen root, we verify with path.relative() that the output path does not traverse above the root. If the output path would escape, block the operation and report an error.

  • In bundleSchemas(modelsDirectory, rootSchemaPath, options = {}), after computing rootSchemaDir and before producing output files, set safeRoot = path.resolve(rootSchemaDir) and then, after generating bundledPath and minifiedPath, check that both output files lie within safeRoot before writing.
  • This can be done by ensuring that path.relative(safeRoot, outputPath) does not start with ".." (or is not absolute).
  • If the check fails, throw an error or return an error response to block unsafe writes.

No new method definitions are needed, but the path validation logic should be added before any write operations.


Suggested changeset 1
tools/src/main/js/bundle-schemas.js

Autofix patch

Autofix patch
Run the following command in your local git repository to apply this patch
cat << 'EOF' | git apply
diff --git a/tools/src/main/js/bundle-schemas.js b/tools/src/main/js/bundle-schemas.js
--- a/tools/src/main/js/bundle-schemas.js
+++ b/tools/src/main/js/bundle-schemas.js
@@ -153,17 +153,28 @@
         console.log(`Models directory: ${absoluteModelsDir}`);
         console.log(`Root schema: ${absoluteRootPath}`);
 
-        // Generate output filenames
+        // Define a safe root directory for all output. Here, it's the directory containing the root schema.
+        const safeRoot = path.resolve(rootSchemaDir);
+
+        // Generate output filenames/paths
         const baseFilename = rootSchemaFilename.replace('.schema.json', '');
         const bundledFilename = `${baseFilename}-bundled.schema.json`;
         const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;
 
-        const bundledPath = path.join(rootSchemaDir, bundledFilename);
-        const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
+        const bundledPath = path.join(safeRoot, bundledFilename);
+        const minifiedPath = path.join(safeRoot, minifiedFilename);
 
+        // Validate that output paths are within safeRoot
+        function isContained(filePath, root) {
+            const rel = path.relative(root, path.resolve(filePath));
+            return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
+        }
+        if (!isContained(bundledPath, safeRoot) || !isContained(minifiedPath, safeRoot)) {
+            throw new Error('Refusing to write output files outside of the safe root directory');
+        }
+
         console.log(`Output (bundled): ${bundledPath}`);
         console.log(`Output (minified): ${minifiedPath}\n`);
-
         // Read all schema files in the models directory
         const files = await fs.readdir(absoluteModelsDir);
         const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled'));
EOF
@@ -153,17 +153,28 @@
console.log(`Models directory: ${absoluteModelsDir}`);
console.log(`Root schema: ${absoluteRootPath}`);

// Generate output filenames
// Define a safe root directory for all output. Here, it's the directory containing the root schema.
const safeRoot = path.resolve(rootSchemaDir);

// Generate output filenames/paths
const baseFilename = rootSchemaFilename.replace('.schema.json', '');
const bundledFilename = `${baseFilename}-bundled.schema.json`;
const minifiedFilename = `${baseFilename}-bundled.min.schema.json`;

const bundledPath = path.join(rootSchemaDir, bundledFilename);
const minifiedPath = path.join(rootSchemaDir, minifiedFilename);
const bundledPath = path.join(safeRoot, bundledFilename);
const minifiedPath = path.join(safeRoot, minifiedFilename);

// Validate that output paths are within safeRoot
function isContained(filePath, root) {
const rel = path.relative(root, path.resolve(filePath));
return rel && !rel.startsWith('..') && !path.isAbsolute(rel);
}
if (!isContained(bundledPath, safeRoot) || !isContained(minifiedPath, safeRoot)) {
throw new Error('Refusing to write output files outside of the safe root directory');
}

console.log(`Output (bundled): ${bundledPath}`);
console.log(`Output (minified): ${minifiedPath}\n`);

// Read all schema files in the models directory
const files = await fs.readdir(absoluteModelsDir);
const schemaFiles = files.filter(file => file.endsWith('.schema.json') && !file.includes('-bundled'));
Copilot is powered by AI and may make mistakes. Always verify output.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CDX 2.0 related to release v2.0

Projects

None yet

Development

Successfully merging this pull request may close these issues.

CycloneDX 2.0

2 participants